/*
 * Copyright 2011-2012 by The Regents of the University of California Licensed
 * under the Apache License, Version 2.0 (the "License"); you may not use this
 * file except in compliance with the License. you may obtain a copy of the
 * License from
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package edu.ucsd.db.bassarisk.schema;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Logger;

import edu.ucsd.db.bassarisk.exception.SchemaValidationException;
import edu.ucsd.db.bassarisk.exception.UnexpectedCaseException;
import edu.ucsd.db.bassarisk.schema.visitor.AttributeClassCollector;
import edu.ucsd.db.bassarisk.schema.visitor.ParentAssignmentVisitor;
import edu.ucsd.db.bassarisk.schema.visitor.RunClassCollector;
import edu.ucsd.db.bassarisk.schema.visitor.SchemaNodeVisitor;
import edu.ucsd.db.bassarisk.schema.visitor.SimplePathNavigator;

public class Schema {
  public static enum Cardinality {
    Cross,
    Meta,
    One,
    Option,
    Star
  }

  public static enum SequenceRelationship {
    AllContainedBy,
    ContainsAll,
    ContainsNone,
    ContainsSome,
    Equals,
    Excludes,
    NoneContainedBy,
    Orthogonal,
    SomeContainedBy
  }

  // FIXME, this ought to come from bassarisk-data
  public static enum ValueType {
    Int32,
    Int64,
    String,
    Enum
  }

  public static SchemaRunNode findAtSimplePath(final SchemaRootNode schema,
                                               final String[] path) {
    SchemaNodeVisitor<SchemaRunNode, Integer> runFinder =
        new SimplePathNavigator(path);
    return schema.accept(runFinder, 0);
  }

  public static SequenceRelationship
      invertRelationship(final SequenceRelationship rela) {
    switch (rela) {
    case AllContainedBy:
      return SequenceRelationship.ContainsAll;
    case SomeContainedBy:
      return SequenceRelationship.ContainsSome;
    case ContainsAll:
      return SequenceRelationship.AllContainedBy;
    case ContainsSome:
      return SequenceRelationship.SomeContainedBy;
    case Excludes:
      return SequenceRelationship.Excludes;
    case Equals:
      return SequenceRelationship.Equals;
    case Orthogonal:
      return SequenceRelationship.Orthogonal;
    case ContainsNone:
      return SequenceRelationship.NoneContainedBy;
    case NoneContainedBy:
      return SequenceRelationship.ContainsNone;
    default:
      throw UnexpectedCaseException.ofEnum(SequenceRelationship.class);
    }
  }

  private boolean isInitialized = false;
  private final Logger LOG = Logger.getLogger(Schema.class.getName());
  private final DataSeqNestingOrder orderMemoization;
  private final SchemaRootNode root;

  public Schema(final SchemaRootNode root) {
    this.root = root;
    this.orderMemoization = new DataSeqNestingOrder();
  }

  public Collection<SchemaAttributeNode>
      collectAttributeNodesForName(final String className) {
    AttributeClassCollector rcc = new AttributeClassCollector(className);
    List<SchemaAttributeNode> runs = new ArrayList<SchemaAttributeNode>();
    this.root.accept(rcc, runs);
    return runs;
  }

  public Collection<SchemaRunNode> collectRunNodesForName(final String className) {
    RunClassCollector rcc = new RunClassCollector(className);
    List<SchemaRunNode> runs = new ArrayList<SchemaRunNode>();
    this.root.accept(rcc, runs);
    return runs;
  }

  public SequenceRelationship findSeqRelationship(final String classNameA,
                                                  final String classNameB)
      throws SchemaValidationException {
    if (!this.isInitialized()) {
      this.initialize();
    }
    if (!this.orderMemoization.contains(classNameA)) {
      this.orderMemoization.addName(this, classNameA);
    }
    if (!this.orderMemoization.contains(classNameB)) {
      this.orderMemoization.addName(this, classNameB);
    }
    return this.orderMemoization.getRelationship(classNameA, classNameB);
  }

  public void initialize() {
    ParentAssignmentVisitor initializer = new ParentAssignmentVisitor();
    this.root.accept(initializer, null);
    this.isInitialized = true;
  }

  private boolean isInitialized() {
    return this.isInitialized;
  }

  public Comparator<DataSequenceClass> sequenceContainmentComparator() {
    return new Comparator<DataSequenceClass>() {
      @Override
      public int compare(final DataSequenceClass rc1, final DataSequenceClass rc2) {
        if (rc1.name.equals(rc2.name)) { return 0; }
        if (rc1.isPositionSpace()) { return -1; }
        if (rc2.isPositionSpace()) { return 1; }
        try {
          SequenceRelationship rela =
              Schema.this.findSeqRelationship(rc1.name, rc2.name);
          switch (rela) {
          case AllContainedBy:
          case SomeContainedBy:
            return -1;
          case ContainsAll:
          case ContainsSome:
            return 1;
          case Excludes:
            return 0;
          case Orthogonal:
            return 0;
          default:
            throw UnexpectedCaseException.ofEnum(SequenceRelationship.class);
          }
        } catch (SchemaValidationException e) {
          String msg =
              "Attempted to compare a run-class (%s, %s) for which the comparator"
                  + " does not have relationship data.";
          msg = String.format(msg, rc1.name, rc2.name);
          Schema.this.LOG.severe(msg);
          throw new RuntimeException(msg, e);
        }
      }
    };
  }
}
